/*

This file is part of roomyak.

Copyright (C) 2008 Pixecur Technologies Incorporated (Esotera).
Visit Esotera at http://www.esoteras3.com
 
roomyak is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3, or (at your option)
any later version.
 
roomyak is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
General Public License for more details.
 
You should have received a copy of the GNU General Public License
along with roomyak; see the file COPYING.  If not, write to 
Pixecur Technologies, 213-31 Peet Street, St. John's, NL, A1B 3W8. 
 
Linking this library statically or dynamically with other modules is
making a combined work based on this library.  Thus, the terms and
conditions of the GNU General Public License cover the whole
combination.
 
*/

package com.datasentinel.util.data;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicLong;

import com.datasentinel.util.CRCHelper;
import com.datasentinel.util.io.StreamUtils;

/**
 * @author Areg Beketovski
 */
public class ReadOnlyByteArray implements Serializable, Comparable<ReadOnlyByteArray>
{
	private static final long serialVersionUID = 1L;

	private static AtomicLong counter = new AtomicLong();
	transient private final long id = counter.incrementAndGet();

	protected byte[] data;

	transient protected InputStream inputStream;



	
	public ReadOnlyByteArray(String value, String encoding) throws UnsupportedEncodingException
	{
		this(value.getBytes(encoding), true);
	}

	/**
	 *  By default this constructor creates a copy of the provided
	 *  data array in order to prevent accidental modification of the referenced
	 *  data array by third parties.
	 *  
	 *  WARNING: Abundant usage of this method might be a source of an excessive memory consumption.
	 * @throws UnsupportedEncodingException 
	 */
	public ReadOnlyByteArray(byte[] data)
	{
		this(data, true);
	}

	protected ReadOnlyByteArray(int size)
	{
		if (size >= 0)
			this.data = new byte[size];
		else
			throw new ArrayIndexOutOfBoundsException("Negative size specified");
	}


	protected ReadOnlyByteArray(byte[] data, boolean createCopy)
	{
		if (data != null)
		{
			if (createCopy)
			{
				this.data = new byte[data.length];
				System.arraycopy(data, 0, this.data, 0, data.length);
			}
			else
			{
				this.data = data;
			}

		}
	}

	public byte get(int index) throws ArrayIndexOutOfBoundsException
	{
		if (this.data == null || index < 0 || index >= length())
			throw new ArrayIndexOutOfBoundsException(index);

		return this.data[index];
	}

	public void get(int offset, byte[] dest, int destPos, int length) throws ArrayIndexOutOfBoundsException
	{
		if (this.data == null)
			throw new ArrayIndexOutOfBoundsException("Empty array");

		if (offset < 0 || length < 0 || destPos < 0 || (destPos + length) > dest.length || (offset + length) > length())
		{
			throw new ArrayIndexOutOfBoundsException("Invalid index/length specified");
		}

		System.arraycopy(data, offset, dest, destPos, length);
	}

	public int length()
	{
		return data == null ? 0 : data.length;
	}

	public InputStream getInputStream()
	{
		return getInputStream(0);
	}

	public InputStream getInputStream(int offset)
	{
		if (this.inputStream == null)
		{
			this.inputStream = createInputStream(offset);
		}

		if (this.inputStream instanceof FixedSizeByteArrayInputStream)
		{
			((FixedSizeByteArrayInputStream)this.inputStream).reset(offset);
		}
		else
		{
			this.inputStream = createInputStream(offset);
		}

		return inputStream;
	}


	protected InputStream createInputStream(int offset)
	{
		return new FixedSizeByteArrayInputStream(data, offset, data.length - offset);
	}

	@Override
	public String toString()
	{
		//		StringBuffer bytesBuffer = new StringBuffer();
		byte[] copy = new byte[16];
		if (data != null)
			System.arraycopy(data, 0, copy, 0, data.length >= 16 ? 16 : data.length);
		return getClass().getSimpleName() + ": " + id + "[" + Arrays.toString(copy) + "]";
	}

	@Override
	public boolean equals(Object obj)
	{
		if (!(obj instanceof ReadOnlyByteArray))
		{
			return false;
		}

		return Arrays.equals(data, ((ReadOnlyByteArray)obj).data);
	}


	public boolean rangeEquals(int offset, ReadOnlyByteArray other, int length)
	{
		if (this == other)
			return true;
		if (other == null)
			return false;

		for (int i = 0; i < length; i++)
			if (data[i + offset] != other.data[i])
				return false;

		return true;
	}


	public boolean rangeEquals(int offset, byte[] other)
	{
		if (other == null)
			return false;
		return rangeEquals(offset, other, other.length);
	}

	public boolean rangeEquals(int offset, byte[] other, int length)
	{
		return rangeEquals(offset, other, 0, length);
	}

	public boolean rangeEquals(int offset, byte[] other, int compareOffset, int length)
	{
		if (data == other)
			return true;
		if (other == null)
			return false;

		if (offset < 0 || compareOffset < 0 || length < 0 || (compareOffset + length) > other.length
				|| (offset + length) > length())
			return false;

		for (int i = 0; i < length; i++)
			if (data[i + offset] != other[i + compareOffset])
				return false;

		return true;
	}


	public short readShort(int offset)
	{
		return (short)(((data[offset] & 0xFF) << 8) + ((data[offset + 1] & 0xFF) << 0));
	}

	public int readInt(int offset)
	{
		return (int)(((data[offset] & 0xFF) << 24) + ((data[offset + 1] & 0xFF) << 16))
				+ (((data[offset + 2] & 0xFF) << 8) + ((data[offset + 3] & 0xFF) << 0));
	}

	public long readLong(int offset)
	{
		return (((long)(data[offset] & 0xFF) << 56) + ((long)((data[offset + 1] & 0xFF)) << 48)
				+ ((long)((data[offset + 2] & 0xFF) & 0xFF) << 40) + ((long)(data[offset + 3] & 0xFF) << 32)
				+ ((long)(data[offset + 4] & 0xFF) << 24) + ((data[offset + 5] & 0xFF) << 16)
				+ ((data[offset + 6] & 0xFF) << 8) + ((data[offset + 7] & 0xFF) << 0));
	}


	public void writeToOutputStream(OutputStream os) throws IOException
	{
		os.write(data);
	}

	public void writeToOutputStream(OutputStream os, int offset, int length) throws IOException
	{
		os.write(data, offset, length);
	}

	public void writeByteArray(OutputStream os) throws IOException
	{
		StreamUtils.writeByteArray(os, data);
	}

	public long getCRC32()
	{
		return CRCHelper.crc32FromBytes(data, 0, length());
	}
	
	public long getCRC32(int offset, int length)
	{
		return CRCHelper.crc32FromBytes(data, offset, length);
	}


	public String readAsciiString() throws UnsupportedEncodingException
	{
		return readAsciiString(data);
	}

	public static String readAsciiString(byte[] data) throws UnsupportedEncodingException
	{
		int length = -1;
		for (int i = 0; i < data.length; ++i)
		{
			if (data[i] == 0)
			{
				length = i;
				break;
			}
		}

		if (length == -1)
		{
			length = data.length;
		}

		return new String(data, 0, length, "US-ASCII");
	}
	
	public String toString(String encoding) throws UnsupportedEncodingException
	{
		return new String(data, encoding);
	}

	public String toArrayString()
	{
		return Arrays.toString(data);
	}
	
	public byte[] toNativeArray()
	{
		byte[] dataCopy = new byte[data.length];
		System.arraycopy(data, 0, dataCopy, 0, data.length);
		return dataCopy;
	}

	public byte[] toNativeArray(int offset, int length)
	{
		byte[] dataCopy = new byte[length];
		System.arraycopy(data, offset, dataCopy, 0, length);
		return dataCopy;
	}

	
	public BigInteger toBigInteger()
	{
		return new BigInteger(data);
	}
	
	@Override
	public int compareTo(ReadOnlyByteArray o)
	{
		if(o == this)
		{
			return 0;
		}
		
		if(o == null)
		{
			return -1;
		}
		
		if(o.data.length != data.length)
		{
			return data.length - o.data.length; 
		}
		
		for(int i = 0; i < data.length; ++i)
		{
			if(o.data[i] != data[i])
			{
				return data[i] - o.data[i];
			}
		}
		
		return 0;
	}
}
